CPS Interpreter in Rust Language
Итак, в прошлых постах мы построили парсер диалектов K, а именно: K3, K4, K5 и Q. Теперь пришла пора написать CPS интерпретатор, такой который смог бы крутить бесконечные циклы лямбдочки быстрее эрланга.
Интерпретатор мы будем строить используя следующий базис понятий функционального программирования: Вложенные контексты, Отложенные вычисления и Лямбда исчисление.
Вложенные контексты — это наш Environment или Dictionary, когда мы создаем дочерний контекст, например при аппликации лямбды, мы линкуем дочерний контекст с родителькому, поэтому в имплментациях акссесоров у нас будет рекурсивный Find — он не только должен вернуть значение по ключу, но и вернуть контекст в котором он это нашел в цепочке контекстов. Значения — это AST (чтобы не плодить много типов, мы просто некоторые конструкторы AST объявляем боксами Integer, String, Boolean, Float, поэтому в процессе работы интерпретатора он вычислит и положит результат в тот же тип AST, в байткоде виртуальной машины — это может выглядеть как просто состояние регистров и инструкции для загрузки или выгрузки констант), а Контексты — это счетчики ссылок на ячейки (AST, Rc<RefCell<Environment>>). Если сказать о контекстах высоко — то вложенные контексты соответствуют Сигма типу и являются свидетелями контекстуальной полноты.
Интерпретатор хранит рутовый контекст.
Отложенные вычисления — используются нами для проскакивания цепочки AST, и форсинг когда достигли определенного события. Обычно во всех базовых функциональных библиотеках (типа PureScript и Elm) называется Lazy Type. У нас Lazy параметризирован контекстами и протоколом выполнения — Environment и Continuation. Вообще тут мистики никакой нет. Если посмотреть на любое выражение — то половина времени тратится на Defer и приблизительно половина на Force, причем все дефер обычно заканчиваются сразу парными форсами. Это сделано для того, чтобы вечные циклы крутились в нулевом стеке. Что еще можно сказать про этот интерпретатор? Я лично хотел добиться от этого интерпретатора идиоматичности, чтобы его понимал и прокурор и милиция. Я намеренно в посте исключил имплементации обычных выражений + и * чтобы показать суть интерпретатора. Так как многие знают что этого уже достаточно для моделирование вычислительных вселенных. По крайней мере такты интерпретатора уже можно считать. Если бы этот интерпретатор был клаудом, он бы чарджил и билил только форсы.
Протокол исполнения: Лямбда исчисление + синтаксические расширения — ядро интерпретатора (Assign, Cond, Lambda, Call) которое создает контексты, делает по ним лукапы и биндит переменные при аппликациях. У нас не аппликативно-божественный (на subst, как в OM/EXE), а нормально-колхозный порядок бета редукции (ALGOL68, LUA, K), зато если поместиться в 32К L1 то можно сорвать большой куш, как это делают K и LuaJIT, остальные сосут с call-by-name причмокивая. Тут хочется быть максимально расширяемым, чтобы новые конструкторы AST ложились на этот протокол отложенных вычислений. Особой мистики тут не нужно, можно считать — это управляющим протоколом, вся магия будет происходить в векторном DSL, там целые блоки будут транслироваться в Lazy структуры итераторы Rust и выполняться в интерпретаторе как единые инструкции, для которых определен AST конструктор. Лямбда исчисление является свидетелем функциоальной полноты нашей системы. Если добавить сюда Пи тип, мы сможем превратить интерпретатор в теорем прувер? :-)
В CPS интерпретаторе самое главное — это цикл, который крутит AST дерево и создает отложенные вычисление, периодически форся вычисления. Обычно это проистодит в стиле Defer-Force, но все зависит от программ.
handle_defer выглядит примерно так:
evaluate_function разворачивает Name и деферит лямбду.
evaluate_expressions создает один отложеный такт интерпретатора, типа R(1).
Обработчики Лямбда исчисления простые, эвалуатор ложит и кладет в контексты, сравниватель сранивает, биндинг переменной биндит переменную.
Вот и весь интерпретатор (кроме правил для Verb и Adverb, там умножение и вычитание необходимые для факториала и Cond вообще говоря тоже, который в K языке повешен на Cast). Традиционно бенчмарки против Erlang написанном на С.
Интерпретатор написанном на Rust:
91 микросекунд (Erlang) против 17 микросекунд (O-CPS INTERPRETER)
ОМ ЛЯМБДА ГАРБХА ХУМ